/** Exhibits standard Lucene searches for ranking documents.
 * 
 * @author Scott Sanner
 */

package search;

import java.io.*;
import java.text.DecimalFormat;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.Version;

public class SimpleSearchRanker {

	String        _indexPath;
	Analyzer      _analyzer; 
	QueryParser   _parser;
	IndexReader   _reader;
	IndexSearcher _searcher;
	DecimalFormat _df = new DecimalFormat("#.####");
	
	public SimpleSearchRanker(String index_path, String default_field, Analyzer a) 
		throws IOException {
		_indexPath = index_path;
		File indexDir = new File(index_path);
		Directory index = new SimpleFSDirectory(indexDir);
		_parser    = new QueryParser(Version.LUCENE_42, default_field, a);
		// Increase the max clause size
		BooleanQuery.setMaxClauseCount(1024 * 32);
		_reader    = DirectoryReader.open(index);
		_searcher  = new IndexSearcher(_reader);
		_analyzer  = a;
	}
	
	public int docId(String filename) throws ParseException, IOException {
		Query q = _parser.parse("PATH:" + filename);
		TopScoreDocCollector collector = TopScoreDocCollector.create(1, true);
		_searcher.search(q, collector);
		ScoreDoc[] hits = collector.topDocs().scoreDocs;
		return hits[0].doc;
	}
	
	public void explain(String query, String filename) throws ParseException, IOException {
		Query q = _parser.parse(query);
		int docId = docId(filename);
		System.out.println("Filename Compare: " + filename);
		String result = _searcher.explain(q, docId).toString();
		String[] lines = result.split("\n");
		
		String keyWord = "";
		double score = 0;
		double queryWeight = 0.0;
		double idf = 0.0;
		boolean queryWeightFlag = false;
		boolean fieldWeightFlag = false;
		int docFreq = 0;
		int maxDocs = 0;
		double fieldWeight = 0.0;
		double termFreq = 0.0;
		double fieldNorm = 0.0;

		System.out.println("filename, keyWord , score , docFreq , maxDocs , fieldWeight , termFreq , fieldNorm");
		for (int i = 0; i < lines.length; i++) {
			String s = lines[i].trim();
			if (lines[i].contains("[DefaultSimilarity]")) {
				keyWord = s.split(" ")[3].split(":")[1];
				score = Double.parseDouble(s.split(" ")[0]);				
			}
			
			if (lines[i].contains("queryWeight, product of")) {
				queryWeight = Double.parseDouble(s.split(" ")[0]);
			}
			
			if (lines[i].contains("idf(docFreq")) {
				idf = Double.parseDouble(s.split(" ")[0]);
				docFreq = Integer.parseInt(s.trim().split(" ")[2].replace("idf(docFreq=", "").replace(",", ""));
				maxDocs = Integer.parseInt(s.trim().split(" ")[3].replace("maxDocs=", "").replace(")", ""));
			}
			
			if (lines[i].contains("fieldWeight")) {
				fieldWeight = Double.parseDouble(s.split(" ")[0]);
			}
			
			if (lines[i].contains("termFreq")) {
				termFreq = Double.parseDouble(s.split(" ")[0]);
			}
			
			if (lines[i].contains("fieldNorm")) {
				fieldNorm = Double.parseDouble(s.split(" ")[0]);
				System.out.println(filename + ", " + keyWord + ", " + score + ", " + docFreq + ", " + maxDocs + ", " + fieldWeight + ", " + termFreq + ", " + fieldNorm);
			}
			
			
		}
		
	}
	
	public void finalize() 
		throws IOException {
//		_searcher.close();
	}
	
//	public void refreshIndex() 
//		throws Exception  {
//
//		if (!_reader.isCurrent()) {
//			
//			// Close the current reader and searcher
//			_reader.close();
//			_searcher.close();
//			_reader = null;
//			_searcher = null;
//			
//			// Optimize the index
//		    IndexWriter w = new IndexWriter(_indexPath, _analyzer, false,
//			        IndexWriter.MaxFieldLength.UNLIMITED);
//	        w.optimize();
//		    w.close();
//
//			// Load a new reader and searcher
//			_reader = IndexReader.open(_indexPath);
//			_searcher = new IndexSearcher(_indexPath);
//		}
//
//	}
	
	public void doSearch(String query, int num_hits, PrintStream ps) 
		throws Exception {
		
		Query q = _parser.parse(query);		
		TopScoreDocCollector collector = TopScoreDocCollector.create(num_hits, true);
		_searcher.search(q, collector);
		ScoreDoc[] hits = collector.topDocs().scoreDocs;

		ps.println("Found " + hits.length + " hits.");
		for (int i = 0; i < hits.length; i++) {
		    int docId = hits[i].doc;
		    Document d = _searcher.doc(docId);
		    ps.println((i + 1) + ". (" + _df.format(hits[i].score) 
		    		   + ") " + d.get("PATH"));
		}
	}
	
	// Return only 1 doc
	public String doSearch(String query)
			throws Exception {
//			System.out.println("DO SEARCH - " + query);
			Query q = _parser.parse(query);
		
			TopScoreDocCollector collector = TopScoreDocCollector.create(1, true);
			_searcher.search(q, collector);
			ScoreDoc[] hits = collector.topDocs().scoreDocs;

//			System.out.println("DO SEARCH - Found " + hits.length + " hits.");
//			for (int i = 0; i < hits.length; i++) {
//			    int docId = hits[i].doc;
//			    Document d = _searcher.doc(docId);
//			    ps.println((i + 1) + ". (" + _df.format(hits[i].score) 
//			    		   + ") " + d.get("PATH"));
//			}
			return _searcher.doc(hits[0].doc).get("PATH");
		}
	public static void main(String[] args) throws Exception {
		
		String index_path = "lucene.index";
		String default_field = "CONTENT";
		
		FileIndexBuilder b = new FileIndexBuilder(index_path);
		SimpleSearchRanker r = new SimpleSearchRanker(b._indexPath, default_field, b._analyzer);
		
		// See the following for query parser syntax
		//   http://lucene.apache.org/java/2_4_1/queryparsersyntax.html
		//
		// See the following site for changing weights of the following:
		//   http://www.lucenetutorial.com/advanced-topics/scoring.html
		//   http://lucene.apache.org/java/2_4_1/api/core/org/apache/lucene/search/Similarity.html
		//   1. tf = term frequency in document = measure of how often a term appears in the document
		//   2. idf = inverse document frequency = measure of how often the term appears across the index
		//   3. coord = number of terms in the query that were found in the document
		//   4. lengthNorm = measure of the importance of a term according to the total number of terms in the field
		//   5. queryNorm = normalization factor so that queries can be compared
		//   6. boost (index) = boost of the field at index-time
		//   7. boost (query) = boost of the field at query-time
		//
		// IN SHORT: the default scoring function for OR terms is a variant of TF-IDF
		//           where one can individually boost the importance of query terms with
		//           a multipler using ^
		//
		// BM25 queries are not shown here, but if desired, the following
		// describes how: 
		//   http://nlp.uned.es/~jperezi/Lucene-BM25/
		// (note that this requires an indexing modification)
		
		// Standard single term
		r.doSearch("Obama", 5, System.out);

		// Multiple term (implicit OR)
		r.doSearch("Obama Hillary", 5, System.out);

		// Wild card
		r.doSearch("Ob*ma", 5, System.out);
		
		// Edit distance
		r.doSearch("Obama~.4", 5, System.out);
		
		// Fielded search (FIELD:...), boolean (AND OR NOT)
		r.doSearch("CONTENT:Obama AND Hillary", 5, System.out);
		r.doSearch("CONTENT:Obama AND NOT Hillary", 5, System.out);

		// Phrase search (slop factor ~k allows words to be within k distance)
		r.doSearch("\"Barack Obama\"", 5, System.out);
		r.doSearch("\"Barack Obama\"~5", 5, System.out);
		
		// Note: can boost terms or subqueries using ^ (e.g., ^10 or ^.1) -- default is 1 
		r.doSearch("Obama^10 Hillary^0.1", 5, System.out);
		r.doSearch("(CONTENT:\"Barack Obama\")^10 OR Hillary^0.1", 5, System.out);

		// Reversing boost... see change in ranking 
		r.doSearch("Obama^0.1 Hillary^10", 5, System.out);
		r.doSearch("(CONTENT:\"Barack Obama\")^0.1 OR Hillary^10", 5, System.out);

		// Complex query
		r.doSearch("(CONTENT:\"Barack Obama\"~5^10 AND Obama~.4) OR Hillary", 5, System.out);
	}

}
